package logx

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"os"
	"os/user"
	"path/filepath"
	"strings"
	"sync"
	"time"

	// "github.com/panjf2000/gnet/pool/ringbuffer"
	// "github.com/panjf2000/gnet/pool/bytebuffer"
	"github.com/panjf2000/gnet/ringbuffer"
	"github.com/tal-tech/go-zero/core/executors"
	"github.com/tal-tech/go-zero/core/fs"

	"github.com/tal-tech/go-zero/core/lang"
	// "github.com/tal-tech/go-zero/core/syncx"
	// "github.com/tal-tech/go-zero/core/timex"
	// "github.com/tal-tech/go-zero/core/collection"
	// "github.com/thinkeridea/go-extend/exstrings"
)

const (
	dateFormat      = "2006-01-02"
	hoursPerDay     = 24
	defaultDirMode  = 0o755
	defaultFileMode = 0o600
)

type (
	// A RotateRule interface is used to define the log rotating rules.
	RotateRule interface {
		BackupFileName() string
		MarkRotated()
		OutdatedFiles() []string
		ShallRotate(int) bool
		SetFileName(string) bool
		MaxSize() int64
	}

	// A RotateLogger is a Logger that can rotate log files with given rules.
	RotateLogger struct {
		filename string
		backup   string
		fp       *os.File
		// channel  chan []byte
		// done     chan lang.PlaceholderType
		rule     RotateRule
		compress bool
		keepDays int
		// can't use threading.RoutineGroup because of cycle import
		// waitGroup sync.WaitGroup
		closeOnce sync.Once

		symlink    string
		logger     *LoggingT
		sev        severity
		nbytes     int // The number of bytes written to this file
		executor   *executors.ChunkExecutor
		ringBuffer *ringbuffer.RingBuffer
		locker     sync.Mutex
	}

	// A DailyRotateRule is a rule to daily rotate the log files.
	DailyRotateRule struct {
		rotatedTime string
		filename    string
		delimiter   string
		days        int
		gzip        bool
		maxSize     int
	}
)

var (
	pid      = os.Getpid()
	program  = filepath.Base(os.Args[0])
	host     = "unknownhost"
	userName = "unknownuser"
	curPath  string
)

func init() {
	curPath, _ = filepath.Abs(os.Args[0])
	curPath = filepath.Dir(curPath)
	h, err := os.Hostname()
	if err == nil {
		host = shortHostname(h)
	}

	current, err := user.Current()
	if err == nil {
		userName = current.Username
	}

	// Sanitize userName since it may contain filepath separators on Windows.
	userName = strings.Replace(userName, `\`, "_", -1)
}

// DefaultRotateRule is a default log rotating rule, currently DailyRotateRule.
func DefaultRotateRule(filename, delimiter string, opt *logOptions) RotateRule {
	// days int, gzip bool
	return &DailyRotateRule{
		rotatedTime: getNowDate(),
		filename:    filename,
		delimiter:   delimiter,
		days:        opt.keepDays,
		gzip:        opt.gzipEnabled,
		maxSize:     opt.maxSize,
	}
}

// BackupFileName returns the backup filename on rotating.
func (r *DailyRotateRule) BackupFileName() string {
	// return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate())
	return fmt.Sprintf("%s%s", r.filename, ".bak")
}

func (r *DailyRotateRule) SetFileName(fname string) bool {
	r.filename = fname
	return true
}

func (r *DailyRotateRule) MaxSize() int64 {
	return int64(r.maxSize)
}

// MarkRotated marks the rotated time of r to be the current time.
func (r *DailyRotateRule) MarkRotated() {
	r.rotatedTime = getNowDate()
}

// OutdatedFiles returns the files that exceeded the keeping days.
func (r *DailyRotateRule) OutdatedFiles() []string {
	if r.days <= 0 {
		return nil
	}

	var pattern string
	if r.gzip {
		pattern = fmt.Sprintf("%s%s*.gz", r.filename, r.delimiter)
	} else {
		pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter)
	}

	files, err := filepath.Glob(pattern)
	if err != nil {
		Errorf("failed to delete outdated log files, error: %s", err)
		return nil
	}

	var buf strings.Builder
	boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat)
	fmt.Fprintf(&buf, "%s%s%s", r.filename, r.delimiter, boundary)
	if r.gzip {
		buf.WriteString(".gz")
	}
	boundaryFile := buf.String()

	var outdates []string
	for _, file := range files {
		if file < boundaryFile {
			outdates = append(outdates, file)
		}
	}

	return outdates
}

// ShallRotate checks if the file should be rotated.
func (r *DailyRotateRule) ShallRotate(nbytes int) bool {
	return (len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime) || (r.maxSize > 0 && nbytes >= r.maxSize)
}

// shortHostname returns its argument, truncating at the first period.
// For instance, given "www.google.com" it returns "www".
func shortHostname(hostname string) string {
	if i := strings.Index(hostname, "."); i >= 0 {
		return hostname[:i]
	}
	return hostname
}

func initFileName(l *LoggingT, tag string) (filename string, link string) {
	t := time.Now()
	paths := l.options.path
	if len(paths) == 0 {
		paths = filepath.Join(curPath, "logs")
	} else if paths[0] != '/' {
		paths = filepath.Join(curPath, paths)
	}

	filename = fmt.Sprintf("%s.log.%s.%04d%02d%02d-%02d%02d%02d-%d.%d",
		l.options.serviceName,
		tag,
		t.Year(),
		t.Month(),
		t.Day(),
		t.Hour(),
		t.Minute(),
		t.Second(),
		t.Nanosecond(),
		pid)
	link = l.options.serviceName + "." + tag

	filename = filepath.Join(paths, filename)
	link = filepath.Join(paths, link)
	return
}

// NewLogger returns a RotateLogger with given filename and rule, etc.
func NewLogger(sev severity, logT *LoggingT, tag string, rule RotateRule, compress bool) (*RotateLogger, error) {
	fname, link := initFileName(logT, tag)
	rule.SetFileName(fname)
	l := &RotateLogger{
		filename:   fname,
		rule:       rule,
		compress:   compress,
		symlink:    link,
		sev:        sev,
		logger:     logT,
		ringBuffer: ringbuffer.New(1024 * 1024 * 1),
	}
	flushtm := logT.options.flushtm
	if flushtm <= 0 {
		flushtm = 5
	}

	l.executor = executors.NewChunkExecutor(func(items []interface{}) {
		l.locker.Lock()
		head, tail := l.ringBuffer.PeekAll()
		if len(head) > 0 {
			n := l.write(head)
			if n == len(head) && len(tail) > 0 {
				n += l.write(tail)
			}
			l.ringBuffer.Discard(n)
		}
		l.locker.Unlock()
		l.Sync()
	}, executors.WithChunkBytes(1024*1020), executors.WithFlushInterval(time.Second*time.Duration(flushtm)))

	if err := l.init(); err != nil {
		return nil, err
	}
	return l, nil
}

func (l *RotateLogger) WriteHeader() {
	now := time.Now()
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "Log file created at: %s\n", now.Format("2006/01/02 15:04:05"))
	fmt.Fprintf(&buf, "Running on machine: %s\n", host)
	// fmt.Fprintf(&buf, "Binary: Built with %s %s for %s/%s\n", runtime.Compiler, runtime.Version(), runtime.GOOS, runtime.GOARCH)
	fmt.Fprintf(&buf, "Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg\n")
	l.fp.Write(buf.Bytes())
	l.Sync()
}

// Close closes l.
func (l *RotateLogger) Close() error {
	var err error
	l.closeOnce.Do(func() {
		// close(l.done)
		if l.executor != nil {
			l.executor.Flush()
			l.executor.Wait()
		}
		// l.waitGroup.Wait()
		if err = l.Sync(); err != nil {
			return
		}
		err = l.fp.Close()
	})

	return err
}

func (l *RotateLogger) Sync() error {
	// l.locker.Lock()
	// defer l.locker.Unlock()
	// fmt.Printf("------sync=[%p]\n",l.fp)
	if l.fp != nil {
		return l.fp.Sync()
	}
	return nil
}
func (l *RotateLogger) Write(data []byte) (int, error) {
	l.locker.Lock()
	l.ringBuffer.Write(data)
	l.locker.Unlock()
	// l.executor.Add(lang.Placeholder, 0)
	l.executor.Add(lang.Placeholder, len(data))

	return len(data), nil
}

func (l *RotateLogger) getBackupFilename() string {
	if len(l.backup) == 0 {
		return l.rule.BackupFileName()
	}

	return l.backup
}

func (l *RotateLogger) init() error {
	l.backup = l.rule.BackupFileName()
	// fmt.Printf("fname=[%q], l.symlink=[%q]\n",l.filename,l.symlink)

	if _, err := os.Stat(l.filename); err != nil {
		basePath := filepath.Dir(l.filename)
		if _, err = os.Stat(basePath); err != nil {
			if err = os.MkdirAll(basePath, defaultDirMode); err != nil {
				return err
			}
		}

		// fmt.Printf("fname=[%q],basePath=[%q], l.backup=[%q]\n",l.filename, basePath,l.backup)

		if l.fp, err = os.Create(l.filename); err != nil {
			return err
		}
	} else if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
		return err
	}
	os.Remove(l.symlink) // ignore err
	err := Truncate(l.filename, l.rule.MaxSize(), l.fp)
	if err != nil {
		return err
	}
	l.WriteHeader()

	os.Symlink(l.filename, l.symlink) // ignore err
	fs.CloseOnExec(l.fp)

	return nil
}

func (l *RotateLogger) maybeCompressFile(file string) {
	if !l.compress {
		return
	}

	defer func() {
		if r := recover(); r != nil {
			// ErrorStack(r)
		}
	}()
	compressLogFile(file)
}

func (l *RotateLogger) maybeDeleteOutdatedFiles() {
	files := l.rule.OutdatedFiles()
	for _, file := range files {
		if err := os.Remove(file); err != nil {
			Errorf("failed to remove outdated file: %s", file)
		}
	}
}

func (l *RotateLogger) postRotate(file string) {
	go func() {
		// we cannot use threading.GoSafe here, because of import cycle.
		l.maybeCompressFile(file)
		l.maybeDeleteOutdatedFiles()
	}()
}

// Truncate changes the size of the file.
func Truncate(path string, capacity int64, f *os.File) error {
	// fileInfo, _ := os.Stat(path)
	// if fileInfo.Size() < capacity {
	// if err := f.Truncate(capacity); err != nil {
	// return err
	// }
	// }
	return nil
}
func (l *RotateLogger) rotate() error {
	if l.fp != nil {
		err := l.fp.Close()
		l.fp = nil
		if err != nil {
			return err
		}
	}
	_, err := os.Stat(l.filename)
	if err == nil && len(l.backup) > 0 {
		backupFilename := l.getBackupFilename()
		err = os.Rename(l.filename, backupFilename)
		if err != nil {
			return err
		}

		l.postRotate(backupFilename)
	}
	os.Remove(l.symlink) // ignore err
	fname, link := initFileName(l.logger, severityName[l.sev])

	//fmt.Printf("===old=[%s],fname=[%q]\n",l.filename,fname)
	l.filename = fname
	l.rule.SetFileName(fname)
	l.symlink = link
	l.backup = l.rule.BackupFileName()
	l.nbytes = 0
	if l.fp, err = os.Create(l.filename); err == nil {
		os.Symlink(l.filename, l.symlink)
		fs.CloseOnExec(l.fp)

		err = Truncate(l.filename, l.rule.MaxSize(), l.fp)
		if err != nil {
			return err
		}
		l.WriteHeader()
	}
	return err
}

func (l *RotateLogger) write(v []byte) (n int) {
	if l.rule.ShallRotate(l.nbytes) {
		if err := l.rotate(); err != nil {
			log.Println(err)
		} else {
			l.rule.MarkRotated()
		}
	}
	if l.fp != nil {
		n, _ = l.fp.Write(v)
		if n > 0 {
			l.nbytes += n
		}
	}
	return
}

func compressLogFile(file string) {
	// start := timex.Now()
	// Infof("compressing log file: %s", file)
	if err := gzipFile(file); err != nil {
		// Errorf("compress error: %s", err)
	} else {
		// Infof("compressed log file: %s, took %s", file, timex.Since(start))
	}
}

func getNowDate() string {
	return time.Now().Format(dateFormat)
}

func gzipFile(file string) error {
	in, err := os.Open(file)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(fmt.Sprintf("%s.gz", file))
	if err != nil {
		return err
	}
	defer out.Close()

	w := gzip.NewWriter(out)
	if _, err = io.Copy(w, in); err != nil {
		return err
	} else if err = w.Close(); err != nil {
		return err
	}

	return os.Remove(file)
}
